package com.nulldev.util.lang.xml;

import java.io.File;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;

public class XML {
	  /**
	   * Item is the base class of everything that can appear in an XML document. 
	   */
	  public static abstract class Item {
	    private Item _parent;

	    /**
	     * Constructs a new Item.
	     */
	    public Item() {
	    }

	    /**
	     * Sets the parent of the this item.
	     * 
	     * @param parent the parent of the this item
	     */
	    protected void setParent(Item parent) {
	      _parent = parent;
	    }

	    /**
	     * Returns the parent of this item, or null if there isn't one.
	     * 
	     * @return the parent of this item, or null if there isn't one
	     */
	    public Item parent() {
	      return _parent;
	    }

	    /**
	     * Returns the XML document, or null if it isn't in a document.
	     *  
	     * @return the XML document, or null if it isn't in a document
	     */
	    public XML.Doc doc() {
	      XML.Item item = this;
	      XML.Item parent = null;
	      while ((parent = item.parent()) != null) {
	        item = parent;
	      }
	      XML.Doc doc = null;
	      if (item instanceof XML.Doc) {
	        doc = (XML.Doc) item;
	      }
	      return doc;
	    }

	    /**
	     * Writes this item to the given writer with a certain indentation. All
	     * items are pretty printed.
	     * 
	     * @param writer the writer to write to
	     * @param indent the current indentation
	     */
	    public abstract void write(PrintWriter writer, int indent);

	    /**
	     * Visits this item and any of its children (if the visitor allows).
	     * 
	     * @param visitor the visitor to visit with
	     */
	    public abstract void visit(XML.Visitor visitor);

	    /**
	     * Writes the given escaped String to the writer.
	     * 
	     * @param value the string value to escape
	     * @param writer the writer to write to
	     */
	    protected void writeEscapedString(String value, PrintWriter writer) {
	      if (value != null) {
	        int length = value.length();
	        for (int i = 0; i < length; i++) {
	          char c = value.charAt(i);
	          switch (c) {
	          case ' ':
	        	writer.print("&nbsp;");
	        	break;
	          case '<':
	            writer.print("&lt;");
	            break;
	          case '>':
	            writer.print("&gt;");
	            break;
	          case '&':
	            writer.print("&amp;");
	            break;
	          case '"':
	            writer.print("&quot;");
	            break;
	          default:
	            writer.print(c);
	          }
	        }
	      }
	    }

	    /**
	     * Writes an indentation to the writer.
	     * 
	     * @param indent the indentation to write
	     * @param writer the writer to write to
	     */
	    protected void writeIndent(int indent, PrintWriter writer) {
	      for (int i = 0; i < indent; i++) {
	        writer.print("  ");
	      }
	    }

	    /**
	     * Generates a formatted representation of this item.
	     * 
	     * @return a formatted representation of this item
	     */
	    @Override
	    public String toString() {
	      StringWriter sw = new StringWriter();
	      PrintWriter pw = new PrintWriter(sw, true);
	      write(pw, 0);
	      return sw.toString();
	    }
	  }

	  /**
	   * Declaration represents an XML declaration (the &lt? ... ?&gt; part of the XML document).
	   * 
	   */
	  public static class Declaration extends Item {
	    private String _version;
	    private String _encoding;

	    /**
	     * Constructs a new Declaration.
	     * 
	     * @param version the version of XML used by this document
	     * @param encoding the encoding used by this document
	     */
	    public Declaration(String version, String encoding) {
	      _version = version;
	      _encoding = encoding;
	    }

	    /**
	     * Returns the version of this declaration.
	     * 
	     * @return the version of this declaration
	     */
	    public String version() {
	      return _version;
	    }

	    /**
	     * Sets the version of this document.
	     * 
	     * @param version the version of this document
	     */
	    public void setVersion(String version) {
	      _version = version;
	    }

	    /**
	     * Returns the encoding for this document.
	     * 
	     * @return the encoding for this document
	     */
	    public String encoding() {
	      return _encoding;
	    }

	    /**
	     * Sets the encoding for this document.
	     * 
	     * @param encoding the encoding for this document
	     */
	    public void setEncoding(String encoding) {
	      _encoding = encoding;
	    }

	    @Override
	    public void visit(Visitor visitor) {
	      visitor.visit(this);
	    }

	    @Override
	    public void write(PrintWriter writer, int indent) {
	      writer.print("<?xml");
	      if (_version != null) {
	        writer.print(" version=\"");
	        writer.print(_version);
	        writer.print("\"");
	      }
	      if (_encoding != null) {
	        writer.print(" encoding=\"");
	        writer.print(_encoding);
	        writer.print("\"");
	      }
	      writer.print("?>");
	      writer.println();
	    }
	  }

	  /**
	   * <p>
	   * Doc represents the top level XML Document object, and is typically the first object you will make when building.
	   * </p>
	   * 
	   * <pre>
	   * XML.Doc doc = XML.doc();
	   * XML.E person = doc.root("person");
	   * </pre>
	   */
	  public static class Doc extends Item {
	    private XML.E _root;
	    private XML.Declaration _declaration;
	    private List<XML.Item> _children;

	    /**
	     * Constructs a new Document.
	     */
	    public Doc() {
	      _children = new LinkedList<XML.Item>();
	      setDeclaration(new XML.Declaration("1.0", "UTF-8"));
	    }

	    /**
	     * Checks if there is already a root element and throws if there is.
	     */
	    protected void checkNullRoot() {
	      if (_root != null) {
	        throw new IllegalStateException("There is already a root node for this document.");
	      }
	    }

	    /**
	     * Returns the declaration for this document, or null if there isn't one. Every Document
	     * gets a version="1.0" encoding="UTF-8" declaration by default, so you should call
	     * setDeclaration(null) if you want to remove this default.
	     * 
	     * @return the declaration for this document
	     */
	    public XML.Declaration declaration() {
	      return _declaration;
	    }

	    /**
	     * Sets the declaration for this document.
	     * 
	     * @param declaration the declaration for this document
	     */
	    public void setDeclaration(XML.Declaration declaration) {
	      if (_declaration != null) {
	        remove(_declaration);
	      }
	      _declaration = declaration;
	      if (_declaration != null) {
	        _declaration.setParent(this);
	        _children.add(0, _declaration);
	      }
	    }

	    /**
	     * Creates a new root element with the given name and returns it. If there is already
	     * a root element, this will throw an exception.
	     * 
	     * @param name the name of the new root element
	     * @return the new root element
	     */
	    public XML.E root(String name) {
	      checkNullRoot();
	      XML.E root = XML.e(name);
	      setRoot(root);
	      return root;
	    }

	    /**
	     * Creates a new root element with the given name and initial text contents and returns 
	     * it. If there is already a root element, this will throw an exception.
	     * 
	     * @param name the name of the new root element
	     * @param value the initial text value of this element
	     * @return the new root element
	     */
	    public XML.E root(String name, String value) {
	      checkNullRoot();
	      XML.E root = XML.e(name, value);
	      setRoot(root);
	      return root;
	    }

	    /**
	     * Returns the root element (or null if there isn't one). Documents do not start out
	     * with a root element, so this might be null.
	     *
	     * @return the root element
	     */
	    public XML.E root() {
	      return _root;
	    }

	    /**
	     * Sets the root element of this document.
	     * 
	     * @param root the new root element
	     */
	    public void setRoot(XML.E root) {
	      if (_root != null) {
	        if (root != null) {
	          int rootIndex = _children.indexOf(_root);
	          _children.set(rootIndex, root);
	        }
	        else {
	          _children.remove(_root);
	        }
	        _root = root;
	      }
	      else if (root != null) {
	        _root = root;
	        _add(root);
	      }
	    }

	    /**
	     * Creates and returns a new comment for this document. If you want comments above
	     * the root element, you should call doc.comment(..) prior to calling .root(..).
	     * 
	     * @param comment a new comment for this document
	     * @return this document
	     */
	    public XML.Doc comment(String comment) {
	      _add(XML.comment(comment));
	      return this;
	    }

	    /**
	     * Removes the given child from this document.
	     * 
	     * @param child the child to remove
	     */
	    public void remove(XML.Item child) {
	      _children.remove(child);
	    }

	    /**
	     * Adds a new child to this document.
	     * 
	     * @param child the child to add
	     */
	    protected void _add(XML.Item child) {
	      child.setParent(this);
	      _children.add(child);
	    }

	    /**
	     * Adds a new child to this document. No validation is performed of these items.
	     * 
	     * @param <T> the type of the item
	     * @param child the child to add
	     * @return the newly added item
	     */
	    public <T extends XML.Item> T add(T child) {
	      if (_root == null && child instanceof XML.E) {
	        setRoot((XML.E) child);
	      }
	      else if (child instanceof XML.Declaration) {
	        setDeclaration((XML.Declaration) child);
	      }
	      else {
	        _add(child);
	      }
	      return child;
	    }

	    /**
	     * Returns the children of this document.
	     * 
	     * @return the children of this document
	     */
	    public List<XML.Item> children() {
	      return _children;
	    }

	    @Override
	    public void write(PrintWriter writer, int indent) {
	      for (XML.Item item : _children) {
	        item.write(writer, indent);
	      }
	    }

	    @Override
	    public void visit(XML.Visitor visitor) {
	      if (visitor.visit(this)) {
	        for (XML.Item item : _children) {
	          item.visit(visitor);
	        }
	      }
	    }

	    public org.w3c.dom.Document w3c() {
	      try {
	        org.w3c.dom.Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
	        if (_children != null) {
	          for (XML.Item child : _children) {
	            if (child instanceof XML.Node) {
	              org.w3c.dom.Node childNode = ((XML.Node) child).w3c(doc);
	              doc.appendChild(childNode);
	            }
	          }
	        }
	        return doc;
	      }
	      catch (Throwable t) {
	        throw new IllegalArgumentException("Failed to create a W3C Document from the this Doc.", t);
	      }
	    }
	  }

	  /**
	   * Node is the abstract superclass of all Node items in a Document.
	   * 
	   * 
	   */
	  public static abstract class Node extends XML.Item {
	    public abstract org.w3c.dom.Node w3c(Document doc);
	  }

	  /**
	   * Content is the abstract superclass of all Nodes that have text content.
	   * 
	   * 
	   */
	  public static abstract class Content extends XML.Node {
	    private String _text;

	    /**
	     * Constructs a new Content.
	     * 
	     * @param text the text of the node
	     */
	    public Content(String text) {
	      _text = text;
	    }

	    /**
	     * Returns the text of this node.
	     * 
	     * @return the text of this node
	     */
	    public String text() {
	      return _text;
	    }

	    /**
	     * Sets text text of this node.
	     * 
	     * @param text text text of this node
	     */
	    public void setText(String text) {
	      _text = text;
	    }

	    @Override
	    public void visit(Visitor visitor) {
	      visitor.visit(this);
	    }

	    /**
	     * Writes the text of this node to the writer.
	     * 
	     * @param writer the writer to write to
	     */
	    protected abstract void writeText(PrintWriter writer);

	    @Override
	    public void write(PrintWriter writer, int indent) {
	      if (_text != null) {
	        writeIndent(indent, writer);
	        writeText(writer);
	      }
	    }
	  }

	  /**
	   * Text represents a bare text node.
	   * 
	   * 
	   */
	  public static class Text extends Content {
	    /**
	     * Creates a new text node.
	     *  
	     * @param text the text of the node
	     */
	    public Text(String text) {
	      super(text);
	    }

	    @Override
	    protected void writeText(PrintWriter writer) {
	      writeEscapedString(text(), writer);
	    }

	    @Override
	    public org.w3c.dom.Node w3c(Document doc) {
	      org.w3c.dom.Text text = doc.createTextNode(text());
	      return text;
	    }
	  }

	  /**
	   * CDATA represents a CDATA section of your document.
	   * 
	   * 
	   */
	  public static class CDATA extends Content {
	    public CDATA(String text) {
	      super(text);
	    }

	    @Override
	    protected void writeText(PrintWriter writer) {
	      writer.print("<![CDATA[");
	      writer.print(text());
	      writer.println("]]>");
	    }

	    @Override
	    public org.w3c.dom.Node w3c(Document doc) {
	      org.w3c.dom.CDATASection cdata = doc.createCDATASection(text());
	      return cdata;
	    }
	  }

	  /**
	   * Comment represents a Comment section of the document.
	   * 
	   * 
	   */
	  public static class Comment extends Content {
	    public Comment(String text) {
	      super(text);
	    }

	    @Override
	    protected void writeText(PrintWriter writer) {
	      writer.print("<!-- ");
	      writer.print(text());
	      writer.println(" -->");
	    }

	    @Override
	    public org.w3c.dom.Node w3c(Document doc) {
	      org.w3c.dom.Comment comment = doc.createComment(text());
	      return comment;
	    }
	  }

	  /**
	   * Attr represents a key-value pair attribute of an element.
	   * 
	   * 
	   */
	  public static class Attr {
	    private String _name;
	    private String _value;

	    /**
	     * Constructs a new attribute.
	     * 
	     * @param name the name of the attribute
	     * @param value the value of the attribute
	     */
	    public Attr(String name, String value) {
	      _name = name;
	      _value = value;
	    }

	    /**
	     * Returns the name of this attribute.
	     * 
	     * @return the name of this attribute
	     */
	    public String name() {
	      return _name;
	    }

	    /**
	     * Sets the name of this attribute.
	     * 
	     * @param name the name of this attribute
	     */
	    public void setName(String name) {
	      _name = name;
	    }

	    /**
	     * Returns the value of this attribute.
	     * 
	     * @return the value of this attribute
	     */
	    public String value() {
	      return _value;
	    }

	    /**
	     * Sets the value of this attribute.
	     * 
	     * @param value the value of this attribute
	     */
	    public void setValue(String value) {
	      _value = value;
	    }
	  }

	  /**
	   * E represents an element of the XML document, which can
	   * have attributes and children nodes.
	   * 
	   * 
	   */
	  public static class E extends XML.Node {
	    private String _name;
	    private List<Node> _children;
	    private List<Attr> _attributes;

	    /**
	     * Constructs a new element.
	     * 
	     * @param name the name of this element
	     */
	    public E(String name) {
	      _name = name;
	    }

	    /**
	     * Constructs a new element.
	     * 
	     * @param name the name of this element
	     * @param text the initial text of this element
	     */
	    public E(String name, String text) {
	      this(name);
	      text(text);
	    }

	    /**
	     * Sets the name of this element.
	     * 
	     * @param name the name of this element
	     */
	    public void setName(String name) {
	      _name = name;
	    }

	    /**
	     * Returns the name of this element.
	     * 
	     * @return the name of this element
	     */
	    public String name() {
	      return _name;
	    }

	    @Override
	    public void visit(XML.Visitor visitor) {
	      if (visitor.visit(this) && _children != null) {
	        for (XML.Node node : _children) {
	          node.visit(visitor);
	        }
	      }
	    }

	    /**
	     * Sets the attribute of the given name to the given value. If there is
	     * already an attribute with this name, it is replaced.
	     * 
	     * @param name the name of the attribute
	     * @param value the value of the attribute
	     * @return this element
	     */
	    public XML.E set(String name, String value) {
	      remove(name);
	      add(new XML.Attr(name, value));
	      return this;
	    }

	    /**
	     * Sets a series of attributes in the format "name1","value1", "name2","value2", ... If any
	     * name already exists, it will be replaced with the value you provide.
	     * 
	     * @param nvPairs an array of name-value pairs
	     * @return this element
	     */
	    public XML.E set(String... nvPairs) {
	      for (int i = 0; i < nvPairs.length; i += 2) {
	        String name = nvPairs[i];
	        String value = nvPairs[i + 1];
	        set(name, value);
	      }
	      return this;
	    }

	    /**
	     * Adds the given attribute to this element. This does
	     * not check for duplicates.
	     * 
	     * @param attribute the attribute to add
	     * @return this element
	     */
	    public XML.E add(XML.Attr attribute) {
	      if (_attributes == null) {
	        _attributes = new LinkedList<XML.Attr>();
	      }
	      _attributes.add(attribute);
	      return this;
	    }

	    /**
	     * Returns the attribute with the given name, or null if there isn't one.
	     * 
	     * @param attributeName the name of the attribute to look up
	     * @return the attribute with the given name, or null if there isn't one
	     */
	    public XML.Attr getAttr(String attributeName) {
	      if (_attributes != null) {
	        for (XML.Attr attribute : _attributes) {
	          if (attribute._name.equals(attributeName)) {
	            return attribute;
	          }
	        }
	      }
	      return null;
	    }

	    /**
	     * Removes the attribute with the given name.
	     * 
	     * @param attributeName the name of the attribute to remove
	     * @return the removed attribute, or null if there wasn't one
	     */
	    public XML.Attr remove(String attributeName) {
	      XML.Attr attribute = getAttr(attributeName);
	      if (attribute != null) {
	        remove(attribute);
	      }
	      return attribute;
	    }

	    /**
	     * Removes the given attribute from this element.
	     * 
	     * @param attribute the attribute to remove
	     */
	    public void remove(XML.Attr attribute) {
	      _attributes.remove(attribute);
	      if (_attributes.size() == 0) {
	        _attributes = null;
	      }
	    }

	    /**
	     * Returns the value of the attribute with the given name, or null if there isn't one.
	     * 
	     * @param attributeName the name of the attribute to look up
	     * @return the value of the attribute with the given name, or null if there isn't one
	     */
	    public String get(String attributeName) {
	      XML.Attr attribute = getAttr(attributeName);
	      String value = null;
	      if (attribute != null) {
	        value = attribute.value();
	      }
	      return value;
	    }

	    /**
	     * Returns the int variant of the value of the attribute with the given name, or null if there isn't one.
	     * 
	     * @param attributeName the name of the attribute to lookup
	     * @param defaultValue the default value to return if there is no attribute, or if the attribute value is null
	     * @return the int variant of the value of the attribute with the given name, or null if there isn't one.
	     */
	    public int getInt(String attributeName, int defaultValue) {
	      int value = defaultValue;
	      String text = get(attributeName);
	    	if (text != null) {
	    		value = Integer.parseInt(text);
	    	}
	      return value;
	    }

	    /**
	     * Returns the float variant of the value of the attribute with the given name, or null if there isn't one.
	     * 
	     * @param attributeName the name of the attribute to lookup
	     * @param defaultValue the default value to return if there is no attribute, or if the attribute value is null
	     * @return the float variant of the value of the attribute with the given name, or null if there isn't one.
	     */
	    public float getFloat(String attributeName, float defaultValue) {
	    	float value = defaultValue;
	      String text = get(attributeName);
	    	if (text != null) {
	     		value = Float.parseFloat(text);
	      }
	      return value;
	    }

	    /**
	     * Returns the boolean variant of the value of the attribute with the given name, or null if there isn't one.
	     * 
	     * @param attributeName the name of the attribute to lookup
	     * @param defaultValue the default value to return if there is no attribute, or if the attribute value is null
	     * @return the boolean variant of the value of the attribute with the given name, or null if there isn't one.
	     */
	    public boolean getBoolean(String attributeName, boolean defaultValue) {
	    	boolean value = defaultValue;
	      String text = get(attributeName);
	    	if (text != null) {
	     		value = Boolean.parseBoolean(text);
	     	}
	      return value;
	    }

	    /**
	     * Returns the enum variant of the value of the attribute with the given name, or null if there isn't one.
	     * 
	     * @param attributeName the name of the attribute to lookup
	     * @param enumClass the class of the enum to return
	     * @param defaultValue the default value to return if there is no attribute, or if the attribute value is null
	     * @return the enum variant of the value of the attribute with the given name, or null if there isn't one.
	     */
	    public <T extends Enum<T>> T getEnum(String attributeName, Class<T> enumClass, T defaultValue) {
	    	T value = defaultValue;
	      String text = get(attributeName);
	    	if (text != null) {
	     		value = Enum.valueOf(enumClass, text);
	      }
	      return value;
	    }

	    /**
	     * Returns the attributes for this element.
	     * 
	     * @return the attributes for this element
	     */
	    public List<XML.Attr> attributes() {
	      return _attributes;
	    }

	    /**
	     * Sets the text value of this element. This will replace an existing Text child with
	     * the provided string, or create a new one if one doesn't exist. If there is already
	     * a child in this element that is not a Text node, this will throw an exception.
	     * 
	     * @param text the new text value for this node
	     * @return this element
	     */
	    public XML.E setText(String text) {
	      if (_children != null) {
	        if (_children.size() == 1) {
	          XML.Node node = _children.get(0);
	          if (node instanceof XML.Text) {
	            ((XML.Text) node).setText(text);
	          }
	          else {
	            throw new IllegalStateException("There was already a non-text child of this element: " + node);
	          }
	        }
	        else {
	          throw new IllegalStateException("There was more than one child of this element: " + this);
	        }
	      }
	      else {
	        text(text);
	      }
	      return this;
	    }

	    /**
	     * Returns the text value of this element, or null if there isn't one. If there is a non-text
	     * child of this element, this will throw an exception.
	     * 
	     * @return the text value of this element
	     */
	    public XML.Text textNode() {
	    	XML.Text textNode = null;
	      if (_children != null) {
	        if (_children.size() == 1) {
	          XML.Node node = _children.get(0);
	          if (node instanceof XML.Text) {
	            textNode = (XML.Text) node;
	          }
	          else {
	            throw new IllegalStateException("There was only a non-text child of this element: " + node);
	          }
	        }
	        else {
	          throw new IllegalStateException("There was more than one child of this element: " + this);
	        }
	      }
	      return textNode;
	    }

	    /**
	     * Returns the text value of this element, or null if there isn't one. If there is a non-text
	     * child of this element, this will throw an exception.
	     * 
	     * @return the text value of this element
	     */
	    public String text() {
	    	XML.Text textNode = textNode();
	      String text = (textNode != null) ? textNode.text() : null;
	      return text;
	    }

	    /**
	     * Returns the children nodes of this element.
	     * 
	     * @return the children nodes of this element
	     */
	    public List<XML.Node> children() {
	      return _children;
	    }

	    /**
	     * Returns the text of the child element with the given name from this element, or null if there isn't one.
	     * 
	     * @param name the name of the element to look up
	     * @return the text of the child element with the given name from this element, or null if there isn't one
	     */
	    public String childText(String name) {
	      XML.E child = child(name);
	      return (child != null) ? child.text() : null;
	    }

	    /**
	     * Returns the int variant of the child element with the given name from this element, or defaultValue if there isn't one.
	     * 
	     * @param name the name of the element to look up
	     * @param defaultValue the default value to return if there is no child node, or if the child node is empty
	     * @return the int variant of the child element with the given name from this element, or defaultValue if there isn't one.
	     */
	    public int childInt(String name, int defaultValue) {
	      int value = defaultValue;
	      XML.E child = child(name);
	      if (child != null) {
	      	String text = child.text();
	      	if (text != null) {
	      		value = Integer.parseInt(text);
	      	}
	      }
	      return value;
	    }

	    /**
	     * Returns the float variant of the child element with the given name from this element, or defaultValue if there isn't one.
	     * 
	     * @param name the name of the element to look up
	     * @param defaultValue the default value to return if there is no child node, or if the child node is empty
	     * @return the float variant of the child element with the given name from this element, or defaultValue if there isn't one.
	     */
	    public float childFloat(String name, float defaultValue) {
	    	float value = defaultValue;
	      XML.E child = child(name);
	      if (child != null) {
	      	String text = child.text();
	      	if (text != null) {
	      		value = Float.parseFloat(text);
	      	}
	      }
	      return value;
	    }

	    /**
	     * Returns the boolean variant of the child element with the given name from this element, or defaultValue if there isn't one.
	     * 
	     * @param name the name of the element to look up
	     * @param defaultValue the default value to return if there is no child node, or if the child node is empty
	     * @return the boolean variant of the child element with the given name from this element, or defaultValue if there isn't one.
	     */
	    public boolean childBoolean(String name, boolean defaultValue) {
	    	boolean value = defaultValue;
	      XML.E child = child(name);
	      if (child != null) {
	      	String text = child.text();
	      	if (text != null) {
	      		value = Boolean.parseBoolean(text);
	      	}
	      }
	      return value;
	    }

	    /**
	     * Returns the enum variant of the child element with the given name from this element, or defaultValue if there isn't one.
	     * 
	     * @param name the name of the element to look up
	     * @param enumClass the class of the enum to return
	     * @param defaultValue the default value to return if there is no child node, or if the child node is empty
	     * @return the enum variant of the child element with the given name from this element, or defaultValue if there isn't one.
	     */
	    public <T extends Enum<T>> T childEnum(String name, Class<T> enumClass, T defaultValue) {
	    	T value = defaultValue;
	      XML.E child = child(name);
	      if (child != null) {
	      	String text = child.text();
	      	if (text != null) {
	      		value = Enum.valueOf(enumClass, text);
	      	}
	      }
	      return value;
	    }

	    /**
	     * Returns the child element with the given name from this element, or null if there isn't one.
	     * 
	     * @param name the name of the element to look up
	     * @return the child element with the given name from this element, or null if there isn't one
	     */
	    public XML.E child(String name) {
	      XML.E matchingElement = null;
	      if (_children != null) {
	        for (XML.Node node : _children) {
	          if (node instanceof XML.E && ((XML.E) node)._name.equals(name)) {
	            if (matchingElement == null) {
	              matchingElement = (XML.E) node;
	            }
	            else {
	              throw new IllegalStateException("There was more than one child named '" + name + "'.");
	            }
	          }
	        }
	      }
	      return matchingElement;
	    }

	    /**
	     * Returns a list of direct children of this element that have the given name, or an empty
	     * list if there aren't any.
	     *  
	     * @param name the name of the children elements to look up
	     * @return a list of direct children of this element that have the given name, or an empty
	     * list if there aren't any
	     */
	    public List<XML.E> children(String name) {
	      List<XML.E> children = new LinkedList<XML.E>();
	      if (_children != null) {
	        for (XML.Node node : _children) {
	          if (node instanceof XML.E && ((XML.E) node)._name.equals(name)) {
	            children.add((XML.E) node);
	          }
	        }
	      }
	      return children;
	    }

	    /**
	     * Returns a set of the text of the descendent elements of this element that have the given name, or an empty
	     * list if there aren't any.
	     *  
	     * @param name the name of the descendent elements to look up
	     * @return a set of the text of the descendents of this element that have the given name, or an empty
	     * set if there aren't any
	     */
	    public Set<String> descendentsText(String name) {
	      Set<XML.E> descendents = descendents(name);
	      Set<String> descendentsText = new LinkedHashSet<String>();
	      for (XML.E descendent : descendents) {
	        descendentsText.add(descendent.text());
	      }
	      return descendentsText;
	    }

	    /**
	     * Returns a set of descendent elements of this element that have the given name, or an empty
	     * list if there aren't any.
	     *  
	     * @param name the name of the descendent elements to look up
	     * @return a set of descendents of this element that have the given name, or an empty
	     * set if there aren't any
	     */
	    public Set<XML.E> descendents(final String name) {
	      final Set<XML.E> descendents = new LinkedHashSet<XML.E>();
	      if (_children != null) {
	        XML.Visitor visitor = new XML.Visitor() {
	          public boolean visit(Item item) {
	            if (item instanceof XML.E && ((XML.E) item)._name.equals(name)) {
	              descendents.add((XML.E) item);
	            }
	            return true;
	          }
	        };

	        for (XML.Node node : _children) {
	          node.visit(visitor);
	        }
	      }
	      return descendents;
	    }

	    /**
	     * Creates and appends a new element to this element.
	     *  
	     * @param name the name of the new element
	     * @return the new element
	     */
	    public XML.E e(String name) {
	      return add(XML.e(name));
	    }

	    /**
	     * Creates and appends a new element to this element.
	     *  
	     * @param name the name of the new element
	     * @param value the text value of the new element
	     * @return the new element
	     */
	    public XML.E e(String name, String value) {
	      return add(XML.e(name, value));
	    }

	    /**
	     * Creates and appends a new text node to this element.
	     *  
	     * @param text the text value to append
	     * @return the new text node
	     */
	    public XML.Text text(String text) {
	      return add(XML.text(text));
	    }

	    /**
	     * Creates and appends a new CDATA node to this element.
	     * 
	     * @param cdata the cdata value to append
	     * @return the new cdata node
	     */
	    public XML.CDATA cdata(String cdata) {
	      return add(XML.cdata(cdata));
	    }

	    /**
	     * Creates and appends a new comment node to this element.
	     * 
	     * @param comment the comment value to append
	     * @return the new comment node
	     */
	    public XML.Comment comment(String comment) {
	      return add(XML.comment(comment));
	    }

	    /**
	     * Removes the given node from this element.
	     * 
	     * @param child the child node to remove
	     */
	    public void remove(XML.Node child) {
	      if (_children != null) {
	        _children.remove(child);
	        if (_children.size() == 0) {
	          _children = null;
	        }
	      }
	    }

	    /**
	     * Adds a new node to this element.
	     * 
	     * @param <T> the type of the node to add
	     * @param child the child to add
	     * @return the added child
	     */
	    public <T extends XML.Node> T add(T child) {
	      child.setParent(this);
	      if (_children == null) {
	        _children = new LinkedList<Node>();
	      }
	      _children.add(child);
	      return child;
	    }

	    /**
	     * Writes the attributes of this element to the writer.
	     * 
	     * @param writer the writer to write attributes to
	     */
	    protected void writeAttributes(PrintWriter writer) {
	      if (_attributes != null) {
	        for (XML.Attr attribute : _attributes) {
	          writer.print(" ");
	          writer.print(attribute.name());
	          writer.print("=\"");
	          writeEscapedString(attribute.value(), writer);
	          writer.print("\"");
	        }
	      }
	    }

	    @Override
	    public void write(PrintWriter writer, int indent) {
	      writeIndent(indent, writer);
	      if (_children != null && _children.size() > 0) {
	        writer.print("<");
	        writer.print(_name);
	        writeAttributes(writer);
	        writer.print(">");
	        if (_children.size() == 1 && _children.get(0) instanceof XML.Text) {
	          _children.get(0).write(writer, 0);
	        }
	        else {
	          writer.println();
	          for (XML.Node node : _children) {
	            node.write(writer, indent + 1);
	          }
	          writeIndent(indent, writer);
	        }
	        writer.print("</");
	        writer.print(_name);
	        writer.println(">");
	      }
	      else {
	        writer.print("<");
	        writer.print(_name);
	        writeAttributes(writer);
	        writer.println(" />");
	      }
	    }

	    @Override
	    public org.w3c.dom.Node w3c(Document doc) {
	      Element e = doc.createElement(_name);
	      if (_attributes != null) {
	        for (XML.Attr attribute : _attributes) {
	          e.setAttribute(attribute.name(), attribute.value());
	        }
	      }
	      if (_children != null) {
	        for (XML.Node child : _children) {
	          org.w3c.dom.Node childNode = child.w3c(doc);
	          e.appendChild(childNode);
	        }
	      }
	      return e;
	    }
	  }

	  /**
	   * Visitor is an interface that can be passed to the visit
	   * method of any XML.Item to walk the DOM.
	   * 
	   * 
	   */
	  public static interface Visitor {
	    /**
	     * Called by the visit method of each item.
	     * 
	     * @param item the item being visited
	     * @return true if the children of the given item should be visited, false if it should skip the children
	     */
	    public boolean visit(XML.Item item);
	  }

	  /**
	   * Creates and returns a new Document.
	   * 
	   * @return a new document
	   */
	  public static XML.Doc doc() {
	    return new XML.Doc();
	  }

	  /**
	   * Creates and return a document parsed from the given string.
	   * 
	   * @param documentString the string to parse as XML
	   * @return a new parsed document
	   */
	  public static XML.Doc doc(String documentString) {
	    try {
	    	XML.Doc doc;
	    	if (documentString == null || documentString.trim().length() == 0) {
	    		doc = XML.doc();
	    	}
	    	else {
	    		Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(documentString)));
	      	doc = XML.doc(document);
	    	}
	    	return doc;
	    }
	    catch (Throwable t) {
	      throw new IllegalArgumentException("Failed to parse a document from the provided string.", t);
	    }
	  }

	  /**
	   * Creates and return a document parsed from the given reader.
	   * 
	   * @param reader the reader to parse from
	   * @return a new parsed document
	   */
	  public static XML.Doc doc(Reader reader) {
	    try {
	      Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(reader));
	      return XML.doc(document);
	    }
	    catch (Throwable t) {
	      throw new IllegalArgumentException("Failed to parse a document from the provided reader.", t);
	    }
	  }

	  /**
	   * Creates and return a document parsed from the given file.
	   * 
	   * @param file the file to parse from
	   * @return a new parsed document
	   */
	  public static XML.Doc doc(File file) {
	    try {
	      Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file);
	      return XML.doc(document);
	    }
	    catch (Throwable t) {
	      throw new IllegalArgumentException("Failed to parse a document from the provided file.", t);
	    }
	  }

	  /**
	   * Converts a W3C Element into an XML.E.
	   * 
	   * @param w3cElement the W3C Element
	   * @return the equivalent XML.E
	   */
	  public static XML.E e(Element w3cElement) {
	    XML.E e = XML.e(w3cElement.getNodeName());
	    org.w3c.dom.NamedNodeMap attributes = w3cElement.getAttributes();
	    for (int i = 0; i < attributes.getLength(); i++) {
	      org.w3c.dom.Node w3cAttribute = attributes.item(i);
	      String attributeName = w3cAttribute.getNodeName();
	      String attributeValue = w3cAttribute.getNodeValue();
	      e.set(attributeName, attributeValue);
	    }
	    org.w3c.dom.NodeList w3cChildren = w3cElement.getChildNodes();
	    for (int i = 0; i < w3cChildren.getLength(); i++) {
	      org.w3c.dom.Node w3cChild = w3cChildren.item(i);
	      if (w3cChild instanceof org.w3c.dom.Text) {
	        e.text(((org.w3c.dom.Text) w3cChild).getNodeValue());
	      }
	      else if (w3cChild instanceof org.w3c.dom.CDATASection) {
	        e.cdata(((org.w3c.dom.CDATASection) w3cChild).getNodeValue());
	      }
	      else if (w3cChild instanceof org.w3c.dom.Comment) {
	        e.comment(((org.w3c.dom.Comment) w3cChild).getNodeValue());
	      }
	      else if (w3cChild instanceof org.w3c.dom.Element) {
	        e.add(XML.e((org.w3c.dom.Element) w3cChild));
	      }
	      else {
	        throw new IllegalArgumentException("Unable to handle nodes of type '" + w3cChild + "'.");
	      }
	    }
	    return e;
	  }

	  /**
	   * Converts a W3C Document into an XML.Doc.
	   * 
	   * @param w3cDocument the W3C Document
	   * @return the equivalent XML.Doc
	   */
	  public static XML.Doc doc(org.w3c.dom.Document w3cDocument) {
	    org.w3c.dom.Element w3cElement = w3cDocument.getDocumentElement();
	    Doc doc = XML.doc();
	    doc.setRoot(XML.e(w3cElement));
	    return doc;
	  }

	  /**
	   * Creates and returns a new Document. The first item that is an element
	   * will be set as the root element of the document.
	   * 
	   * @param children the children to add to the document
	   * @return a new element
	   */
	  public static XML.Doc doc(XML.Item... children) {
	    XML.Doc doc = XML.doc();
	    for (XML.Item child : children) {
	      doc.add(child);
	    }
	    return doc;
	  }

	  /**
	   * Creates and returns a new Declaration.
	   * 
	   * @param version the version of the declaration
	   * @param encoding the encoding of the declaration
	   * @return a new declaration
	   */
	  public static XML.Declaration declaration(String version, String encoding) {
	    return new XML.Declaration(version, encoding);
	  }

	  /**
	   * Creates and returns a new Element.
	   * 
	   * @param name the name of the element
	   * @return a new element
	   */
	  public static XML.E e(String name) {
	    return new XML.E(name);
	  }

	  /**
	   * Creates and returns a new Element.
	   * 
	   * @param name the name of the element
	   * @param children the children to add to this element (String, XML.Node, or XML.Attr)
	   * @return a new element
	   */
	  public static XML.E e(String name, Object... children) {
	    XML.E e = XML.e(name);
	    for (Object child : children) {
	      if (child instanceof String) {
	        e.text((String) child);
	      }
	      else if (child instanceof XML.Node) {
	        e.add((XML.Node) child);
	      }
	      else if (child instanceof XML.Attr) {
	        e.add((XML.Attr) child);
	      }
	      else {
	        throw new IllegalArgumentException("Unable to add the object '" + child + "' to an XML element.");
	      }
	    }
	    return e;
	  }

	  /**
	   * Creates and returns a new Element.
	   * 
	   * @param name the name of the element
	   * @param text the text of the element
	   * @return a new element
	   */
	  public static XML.E e(String name, String text) {
	    return new XML.E(name, text);
	  }

	  /**
	   * Creates and returns a new attribute.
	   * 
	   * @param name the name of the attribute
	   * @param value the value of the attribute
	   * @return a new attribute
	   */
	  public static XML.Attr a(String name, String value) {
	    return new XML.Attr(name, value);
	  }

	  /**
	   * Creates and returns a new text node.
	   * 
	   * @param text the text of the node
	   * @return a new text node
	   */
	  public static XML.Text text(String text) {
	    return new XML.Text(text);
	  }

	  /**
	   * Creates and returns a new cdata node.
	   * 
	   * @param text the text of the cdata node
	   * @return a new cdata node
	   */
	  public static XML.CDATA cdata(String cdata) {
	    return new XML.CDATA(cdata);
	  }

	  /**
	   * Creates and returns a new comment node.
	   * 
	   * @param text the text of the comment node
	   * @return a new comment node
	   */
	  public static XML.Comment comment(String comment) {
	    return new XML.Comment(comment);
	  }
}
